import java.util.ArrayList;

/*
 * class Parser
 * 
 * Parses the tokens returned from the lexical analyzer.
 * 
 * Update the parser implementation to parse the Decaf
 * language according to the grammar provided. 
 */
public class Parser {

	private Lexer lexer; // lexical analyzer
	private Token token; // look ahead token
	
	public Parser(Lexer lex) {
		lexer = lex;
	}
	
	public ArrayList<ClassDecl> parse() throws SyntaxException {
		ArrayList<ClassDecl> cls = new ArrayList<ClassDecl>();
		token = lexer.nextToken();
		// //System.out.println("Next token is " + token.getTokenType() + " " + token.getLexeme());

		while(token.getTokenType() != Token.EOF) {
			cls.add(classDecl());
		}
		
		return cls;
	}

	private ClassDecl classDecl() throws SyntaxException{
		String cl = token.getLexeme();
		match(Token.CLASS);
		String id = token.getLexeme();
		match(Token.ID);
		String lb = token.getLexeme();
		match(Token.LB);
		MethodDecls m = methodDecls();
		String rb = token.getLexeme();
		match(Token.RB);
		return new ClassDecl(cl, id, lb, m, rb);
	}

	private MethodDecls methodDecls() throws SyntaxException{
		return new MethodDecls(methodDecls2());
	}

	private MethodDecls2 methodDecls2() throws SyntaxException{
		MethodDecls2 value = null;
		if (token.getTokenType() == Token.STATIC) {
			MethodDecl m  = methodDecl();
			MethodDecls2 m2 = methodDecls2();
			value = new MethodDecls2(m, m2);
		} else {
			value = null;
		}
		return value;
	}

	private MethodDecl methodDecl() throws SyntaxException{
		String st = token.getLexeme();
		match(Token.STATIC);
		Type t = type();
		String id = token.getLexeme();
		SymbolTable table = SymbolTable.getInstance();
		table.addMethod(id);
		match(Token.ID);
		String lp = token.getLexeme();
		match(Token.LP);
		FormalParams fp = formalParams();
		String rp = token.getLexeme();
		match(Token.RP);
		Block b = block();
		return new MethodDecl(st, t, id, lp, fp, rp, b);
	}

	private Type type() throws SyntaxException{
		String type = token.getLexeme();
		if(token.getTokenType() == Token.INT)
			match(Token.INT);
		else if(token.getTokenType() == Token.FLOAT)
			match(Token.FLOAT);
		else if(token.getTokenType() == Token.BOOLEAN)
			match(Token.BOOLEAN);
		else if(token.getTokenType() == Token.STRING)
			match(Token.STRING);
		else {
			// Because the charNumber is incremented after each read
			lexer.charNumber -= 1;
			throw new SyntaxException("Syntax Error at line " + lexer.lineNumber + " char " + lexer.charNumber + ":\nCannot match '"
				 + token.getLexeme() + "' to a type in:\n" + lexer.lines.get(lexer.lineNumber));
		}
		return new Type(type);
	}

	private FormalParams formalParams() throws SyntaxException{
		FormalParams f;
		if (token.getTokenType() == Token.INT
			|| token.getTokenType() == Token.FLOAT
			|| token.getTokenType() == Token.BOOLEAN
			|| token.getTokenType() == Token.STRING) {
			f = new FormalParams(properFormalParams());
		} else {
			f = null;
		}

		return f;
	}

	private ProperFormalParams properFormalParams() throws SyntaxException{
		FormalParam fp = formalParam();
		Proper2 p = proper2();
		return new ProperFormalParams(fp, p);
	}

	private Proper2 proper2() throws SyntaxException{
		if (token.getTokenType() == Token.FA) {
			String fa = token.getLexeme();
			match(Token.FA);
			FormalParam fp = formalParam();
			Proper2 p = proper2();
			return new Proper2(fa, fp, p);
		}
		else {
			return null;
		}
	}

	private FormalParam formalParam() throws SyntaxException{
		Type type  = type();
		String id = token.getLexeme();
		// //System.out.println("output "+ type.type + " " + id);
		SymbolTable table = SymbolTable.getInstance();
		table.addMethod(type.type + " " + id);
		match(token.ID);
		return new FormalParam(type, id);
	}

	private Block block() throws SyntaxException{
		String lb = token.getLexeme();
		match(Token.LB);
		Statements sts = statements();
		String rb = token.getLexeme();
		match(Token.RB);
		return new Block(lb, sts, rb);
	}

	private Statements statements() throws SyntaxException{
		Statements2 sts2 = statements2();
		return new Statements(sts2);
	}

	private Statements2 statements2() throws SyntaxException{
		if (token.getTokenType() == Token.INT
			|| token.getTokenType() == Token.FLOAT
			|| token.getTokenType() == Token.BOOLEAN
			|| token.getTokenType() == Token.STRING
			|| token.getTokenType() == Token.IF
			|| token.getTokenType() == Token.LB
			|| token.getTokenType() == Token.RETURN
			|| token.getTokenType() == Token.WHILE
			|| token.getTokenType() == Token.ID) {
			Statement st = statement();
			Statements2 sts2 = statements2();
			return new Statements2(st, sts2);
		} else {
			return null;
		}
	}

	private Statement statement() throws SyntaxException{
		if (token.getTokenType() == Token.INT
			|| token.getTokenType() == Token.FLOAT
			|| token.getTokenType() == Token.BOOLEAN
			|| token.getTokenType() == Token.STRING) {
			LocalVarDecl lvd = localVarDecl();
			return new Statement(null, lvd, null, null, null, null);
		} else if (token.getTokenType() == Token.IF) {
			IfStmt is = ifStmt();
			return new Statement(null, null, null, is, null, null);
		} else if (token.getTokenType() == Token.LB) {
			Block b = block();
			return new Statement(b, null, null, null, null, null);
		} else if (token.getTokenType() == Token.RETURN) {
			ReturnStmt rs = returnStmt();
			return new Statement(null, null, null, null, null, rs);
		} else if (token.getTokenType() == Token.WHILE) {
			WhileStmt ws = whileStmt();
			return new Statement(null, null, null, null, ws, null);
		} else if (token.getTokenType() == Token.ID) {
			AssignStmt as = assignStmt();
			return new Statement(null, null, as, null, null, null);
		} else {
			throw new SyntaxException("Syntax Error at line " + lexer.lineNumber + " char " + lexer.charNumber + ":\nCannot match '"
				 + token.getLexeme() + "' to a statement in:\n" + lexer.lines.get(lexer.lineNumber));
		}
		// return null;
	}

	private LocalVarDecl localVarDecl() throws SyntaxException{
		Type type = type();
		String id = token.getLexeme();
		match(Token.ID);
		String sm = token.getLexeme();
		match(Token.SM);
		return new LocalVarDecl(type, id, sm);
	}

	private IfStmt ifStmt() throws SyntaxException{
		String IF = token.getLexeme();
		match(Token.IF);
		String lp = token.getLexeme();
		match(Token.LP);
		Expression exp = expression();
		String rp = token.getLexeme();
		match(Token.RP);
		Statement st = statement();
		IfStmt2 IF2 = ifStmt2();
		return new IfStmt(IF, lp, exp, rp, st, IF2);
	}

	private IfStmt2 ifStmt2() throws SyntaxException{
		if (token.getTokenType() == Token.ELSE) {
			String el = token.getLexeme();
			match(token.getTokenType());
			Statement st= statement();
			return new IfStmt2(el, st);
		} else {
			return null;
		}
		
	}

	private AssignStmt assignStmt() throws SyntaxException{
		String id = token.getLexeme();
		match(Token.ID);
		String ao = token.getLexeme();
		match(Token.AO);
		Expression exp = expression();
		String sm = token.getLexeme();
		match(Token.SM);
		return new AssignStmt(id, ao, exp, sm);
	}

	private WhileStmt whileStmt() throws SyntaxException{
		String wh = token.getLexeme();
		match(Token.WHILE);
		String lp = token.getLexeme();
		match(Token.LP);
		Expression exp = expression();
		String rp = token.getLexeme();
		match(Token.RP);
		Statement st = statement();
		return new WhileStmt(wh, lp, exp, rp, st);
	}

	private ReturnStmt returnStmt() throws SyntaxException{
		String rt = token.getLexeme();
		match(Token.RETURN);
		Expression exp = expression();
		String sm = token.getLexeme();
		match(Token.SM);
		return new ReturnStmt(rt, exp, sm);
	}

	private Expression expression() throws SyntaxException{
		// //System.out.println("EXPR token" + token.getTokenType() + " " + token.getLexeme());
		ConditionalAndExpr cae = conditionalAndExpr();
		Expression2 exp2 = expression2();
		return new Expression(cae, exp2);
	}

	private Expression2 expression2() throws SyntaxException{
		if (token.getTokenType() == Token.LO) {
			String lo = token.getLexeme();
			match(token.getTokenType());
			ConditionalAndExpr cae = conditionalAndExpr();
			Expression2 exp2 = expression2();
			return new Expression2(lo, cae, exp2);
		} else {
			return null;
		}
		
	}

	private ConditionalAndExpr conditionalAndExpr() throws SyntaxException{
		EqualityExpr ee = equalityExpr();
		ConditionalAndExpr2 cae2 = conditionalAndExpr2();
		return new ConditionalAndExpr(ee, cae2);
	}

	private ConditionalAndExpr2 conditionalAndExpr2() throws SyntaxException{
		if (token.getTokenType() == Token.LA) {
			String la = token.getLexeme();
			match(token.getTokenType());
			EqualityExpr ee = equalityExpr();
			ConditionalAndExpr2 cae2 = conditionalAndExpr2();
			return new ConditionalAndExpr2(la, ee, cae2);
		} else {
			return null;
		}
		
	}

	private EqualityExpr equalityExpr() throws SyntaxException{
		AdditiveExpr ae = additiveExpr();
		EqualityExpr2 ee2 = equalityExpr2();
		return new EqualityExpr(ae, ee2);
	}

	private EqualityExpr2 equalityExpr2() throws SyntaxException{
		
		if (token.getTokenType() == Token.EQ
			|| token.getTokenType() == Token.NE) {
			EqualityExpr3 ee3 = equalityExpr3();
			EqualityExpr2 ee2 = equalityExpr2();
			return new EqualityExpr2(ee3, ee2);
		} else {
			return null;
		}
		
	}

	private EqualityExpr3 equalityExpr3() throws SyntaxException{
		boolean value = true;
		if (token.getTokenType() == Token.EQ) {
			String eq = token.getLexeme();
			match(token.getTokenType());
			AdditiveExpr ae = additiveExpr();
			return new EqualityExpr3(eq, null, ae);
		} else if (token.getTokenType() == Token.NE) {
			String ne = token.getLexeme();
			match(token.getTokenType());
			AdditiveExpr ae = additiveExpr();
			return new EqualityExpr3(null, ne, ae);
		} else {
			throw new SyntaxException("Syntax Error at line " + lexer.lineNumber + " char " + lexer.charNumber + ":\nCannot match '"
				 + token.getLexeme() + "' to a '==' or '!=' in:\n" + lexer.lines.get(lexer.lineNumber));
		}
		// return null;
	}

	private AdditiveExpr additiveExpr() throws SyntaxException{
		MultiplicativeExpr me = multiplicativeExpr();
		AdditiveExpr2 ae2 = additiveExpr2();
		return new AdditiveExpr(me, ae2);
	}

	private AdditiveExpr2 additiveExpr2() throws SyntaxException{
		boolean value = true;
		if (token.getTokenType() == Token.PO
			|| token.getTokenType() == Token.MO) {
			AdditiveExpr3 ae3 = additiveExpr3();
			AdditiveExpr2 ae2 = additiveExpr2();
			return new AdditiveExpr2(ae3, ae2);
		} else {
			return null;
		}
	}

	private AdditiveExpr3 additiveExpr3() throws SyntaxException{
		if (token.getTokenType() == Token.PO) {
			String po = token.getLexeme();
			match(Token.PO);
			MultiplicativeExpr me = multiplicativeExpr();
			return new AdditiveExpr3(po, null, me);
		} else if (token.getTokenType() == Token.MO) {
			String mo = token.getLexeme();
			match(Token.MO);
			MultiplicativeExpr me = multiplicativeExpr();
			return new AdditiveExpr3(null, mo, me);
		} else {
			throw new SyntaxException("Syntax Error at line " + lexer.lineNumber + " char " + lexer.charNumber + ":\nCannot match '"
				 + token.getLexeme() + "' to a '+' or '-' in:\n" + lexer.lines.get(lexer.lineNumber));
		}
		// return null;
	}

	private MultiplicativeExpr multiplicativeExpr() throws SyntaxException{
		PrimaryExpr pe = primaryExpr();
		MultiplicativeExpr2 me2= multiplicativeExpr2();
		return new MultiplicativeExpr(pe, me2);
	}

	private MultiplicativeExpr2 multiplicativeExpr2() throws SyntaxException{
		boolean value = true;
		if (token.getTokenType() == Token.TO
			|| token.getTokenType() == Token.DO
			|| token.getTokenType() == Token.MD) {
			MultiplicativeExpr3 me3 = multiplicativeExpr3();
			MultiplicativeExpr2 me2 = multiplicativeExpr2();
			return new MultiplicativeExpr2(me3, me2);
		} else {
			return null;
		}
	}

	private MultiplicativeExpr3 multiplicativeExpr3() throws SyntaxException{
		boolean value = true;
		if (token.getTokenType() == Token.TO) {
			String to = token.getLexeme();
			match(Token.TO);
			PrimaryExpr pe = primaryExpr();
			return new MultiplicativeExpr3(to, null, null, pe);
		} else if (token.getTokenType() == Token.DO) {
			String Do = token.getLexeme();
			match(Token.DO);
			PrimaryExpr pe= primaryExpr();
			return new MultiplicativeExpr3(null, Do, null, pe);
		} else if (token.getTokenType() == Token.MD) {
			String mo = token.getLexeme();
			match(Token.MD);
			PrimaryExpr pe = primaryExpr();
			return new MultiplicativeExpr3(null, null, mo, pe);
		} else {
			throw new SyntaxException("Syntax Error at line " + lexer.lineNumber + " char " + lexer.charNumber + ":\nCannot match '"
				 + token.getLexeme() + "' to a '*' or '/' or '%' in:\n" + lexer.lines.get(lexer.lineNumber));
		}
		// return null;
	}

	private PrimaryExpr primaryExpr() throws SyntaxException{
		if (token.getTokenType() == Token.NM) {
			String nm = token.getLexeme();
			match(token.getTokenType());
			return new PrimaryExpr(nm, null, null, null, null, null, null, null);
		} else if (token.getTokenType() == Token.BL) {
			String bl = token.getLexeme();
			match(token.getTokenType());
			return new PrimaryExpr(null, bl, null, null, null, null, null, null);
		} else if (token.getTokenType() == Token.ST) {
			String st = token.getLexeme();
			match(token.getTokenType());
			return new PrimaryExpr(null, null, st, null, null, null, null, null);
		} else if (token.getTokenType() == Token.ID) {
			String id = token.getLexeme();
			match(token.getTokenType());
			if (token.getTokenType() == Token.LP) {
				CallExpr ce = callExpr(id);
				return new PrimaryExpr(null, null, null, null, ce, null, null, null);
			}
			return new PrimaryExpr(null, null, null, id, null, null, null, null);
		} else if (token.getTokenType() == Token.LP) {
			String lp = token.getLexeme();
			match(Token.LP);
			Expression exp = expression();	
			String rp = token.getLexeme();
			match(Token.RP);
			return new PrimaryExpr(null, null, null, null, null, lp, exp, rp);
		} else {
			throw new SyntaxException("Syntax Error at line " + lexer.lineNumber + " char " + lexer.charNumber + ":\nCannot match '"
				 + token.getLexeme() + "' to a primary expression in:\n" + lexer.lines.get(lexer.lineNumber));
		}
		// return null;
	}

	private CallExpr callExpr(String id) throws SyntaxException{
		String lp = token.getLexeme();
		match(Token.LP);
		ActualParams ap = actualParams();
		String rp = token.getLexeme();
		match(Token.RP);
		return new CallExpr(id, lp, ap, rp);
	}

	private ActualParams actualParams() throws SyntaxException{
		ProperActualParams pap = properActualParams();
		return new ActualParams(pap);
	}

	private ProperActualParams properActualParams() throws SyntaxException{
		Expression exp = expression();
		ProperActualParams2 pap2 = properActualParams2();
		return new ProperActualParams(exp, pap2);
	}

	private ProperActualParams2 properActualParams2() throws SyntaxException{
		boolean value = true;
		if (token.getTokenType() == Token.FA) {
			String fa = token.getLexeme();
			match(token.getTokenType());
			Expression exp = expression();
			ProperActualParams2 pap2 = properActualParams2();
			return new ProperActualParams2(fa, exp, pap2);
		} else {
			return null;
		}
	}

	private void match(int t) throws SyntaxException {
		if (token.getTokenType() == t) {
			token = lexer.nextToken();
		} else {
			// // Because the charNumber is incremented after each read
			// lexer.charNumber -= 1;

			// System.out.println("Syntax Error at line " + lexer.lineNumber + " char " + lexer.charNumber);			
			// System.out.println("Expected '" + new Token(t).getLexeme() + "'");
			// System.out.println("Found '" + token.getLexeme() + "'");
			// System.out.println("In: " + lexer.lines.get(lexer.lineNumber));
			throw new SyntaxException("Syntax Error at line " + lexer.lineNumber + " char " + lexer.charNumber +
				"\nExpected '" + new Token(t).getLexeme() + "'" + "\nFound '" + token.getLexeme() + "'" + 
				"\nIn: " + lexer.lines.get(lexer.lineNumber));
		}
	}
}
